/*
 * DEMElement.cpp
 *
 *  Created on: 07/mar/2013
 *      Author: lordmzn
 */

#include "MOLEM/DEMElement.h"

namespace MOLEM {

Direction::DirEnum getDirection(const Direction::DirEnum& dir,
		const Direction::SecDirEnum& sec) {
	if (dir == Direction::NONE) {
		return Direction::NONE;
	}
	int idxDir = (int) dir + (int) sec;
	if (idxDir == 9) { // out of bounds
		idxDir = 1;
	} else if (idxDir == 0) { // Dir::NONE
		idxDir = 8;
	}
	return Direction::allDirections[idxDir];
}

DEMElement::DEMElement(unsigned int xx, unsigned int yy) :
		Element(xx, yy), goesInto(0x0), secDir(Direction::THIS), gradient(
				GRADIENT_DEFAULT_VALUE), comesFrom(), elementsAccumulated(
				DISCRETE_ACC_VALUE), isSpring(false), isSink(false) {
}

DEMElement::DEMElement(unsigned int xx, unsigned int yy,
		Direction::DirEnum boundary) :
		Element(xx, yy, boundary), goesInto(0x0), secDir(Direction::THIS), gradient(
				GRADIENT_DEFAULT_VALUE), comesFrom(), elementsAccumulated(
				DISCRETE_ACC_VALUE), isSpring(false), isSink(false) {
}

DEMElement::DEMElement(const DEMElement& g) :
		Element(g), goesInto(g.goesInto), secDir(g.secDir), gradient(
				g.gradient), comesFrom(g.comesFrom), elementsAccumulated(
				g.elementsAccumulated), isSpring(g.isSpring), isSink(g.isSink) {
}

DEMElement::~DEMElement() {
}

bool DEMElement::operator==(const DEMElement& a) const {
	// check self
	if (this == &a) {
		return true;
	}
	// check fields
	if (Element::operator!=(a)) {
		return false;
	}
	if (goesInto != a.goesInto || secDir != a.secDir) {
		return false;
	}
	if (gradient != a.gradient) {
		return false;
	}
	if (comesFrom != a.comesFrom) {
		return false;
	}
	if (elementsAccumulated != a.elementsAccumulated) {
		return false;
	}
	if (isSpring != a.isSpring || isSink != a.isSink) {
		return false;
	}
	return true;
}

bool DEMElement::operator !=(const DEMElement& a) const {
	return !(*this == a);
}

bool DEMElement::operator<(const DEMElement& rv) const {
	return getZvalue() < rv.getZvalue();
}

void DEMElement::setZvalue(double v) {
	if (Element::getZvalue() == ELEMENT_Z_DEFAULT_VALUE) {
		Element::setZvalue(v);
		return;
	}
	if (Element::getZvalue() == v) {
		return;
	}
	// set z and reset the rest
	Element::setZvalue(v);
	goesInto = 0x0;
	secDir = Direction::THIS;
	gradient = GRADIENT_DEFAULT_VALUE;
	comesFrom.clear();
	isSink = false;
	isSpring = false;
	elementsAccumulated = DISCRETE_ACC_VALUE;
}

// this is a spring: direction defined by D8
void DEMElement::findDirection() {
	if (!isDirectionEvaluated() && isSpring) {
		Direction::DirEnum dir = findLocalSteepestDirection();
		if (!isSink) {
			findSecondaryDirection(dir);
			goesInto->setAccumulation(*this);
			goesInto->findDirection(*this, dir);
		}
	}
}

// this isn't a spring
void DEMElement::findDirection(DEMElement& source, Direction::DirEnum primDir) {
	if (!isDirectionEvaluated()) {
		addDrainsFrom(source);
		bool updateSource = false;
		Direction::DirEnum dir = findLocalSteepestDirection();
		if (!isSink) {
			double gradientToSecondaryDirectionCell = findSecondaryDirection(
					dir);
			// choose direction
			if (secDir == Direction::THIS) {
				updateSource = true;
			} else if (primDir == dir && source.secDir == secDir) {
				// compare gradients from source in order to choose
				if (source.evaluateGradient(
						static_cast<DEMElement&>(getNeighbour(
								getDirection(dir, secDir))))
						> source.evaluateGradient(
								static_cast<DEMElement&>(getNeighbour(dir)))) { // is the one from D8
					setGoesInto(
							(DEMElement&) getNeighbour(
									getDirection(primDir, secDir)));
					gradient = gradientToSecondaryDirectionCell;
				}
			} // else everything is already set
			goesInto->setAccumulation(*this);
			updateSource ?
					goesInto->findDirection(*this, dir) :
					goesInto->findDirection(source, dir); // keep searching
		}
	} else {
		addDrainsFrom(source);

		// TODO corrects the already evaluated direction with the new one (and the
		// new source) => could be simplified if direction retrieval starts from the
		// top cells.
		// direction has been already evaluated but may require a correction
		// search for the upward el with least gradient: it will be
		// considered as the main contributor (but this will trigger a chain...)
//		double grad_t = GRADIENT_MAX_VALUE;
//		GradientableElement* source;
//		for (std::list<GradientableElement*>::iterator prevEl =
//				comesFrom.begin(); prevEl != comesFrom.end(); ++prevEl) {
//			if ((*prevEl)->gradient < grad_t) {
//				grad_t = (*prevEl)->gradient;
//				source = (*prevEl);
//			}
//		}
	}
}

double DEMElement::findSecondaryDirection(const Direction::DirEnum& d) {
	double clockwiseGrad = evaluateGradient(
			static_cast<DEMElement&>(getNeighbour(
					getDirection(d, MOLEM::Direction::CLOCKWISE))));
	double counterclockGrad = evaluateGradient(
			static_cast<DEMElement&>(getNeighbour(
					getDirection(d, MOLEM::Direction::COUNTERWISE))));
	// if the secondary gradient is negative, the cell is in a canyon and
	// becomes the source for the downward flow.
	if (clockwiseGrad < 0 && counterclockGrad < 0) {
		secDir = MOLEM::Direction::THIS;
		return 0.0;
	}
	if (clockwiseGrad > counterclockGrad) {
		secDir = Direction::CLOCKWISE;
		return clockwiseGrad;
	}
	secDir = Direction::COUNTERWISE;
	return counterclockGrad;
}

bool DEMElement::isASpring() {
	return isSpring;
}

bool DEMElement::isASink() {
	return isSink;
}

bool DEMElement::isDirectionEvaluated() {
	if (comesFrom.size() == 0
			|| (comesFrom.size() == 1 && comesFrom.front() == 0x0)) {
		if (goesInto == 0x0) {
			return false;
		}
	}
	return true;
}

bool DEMElement::isGradientEvaluated() {
	return gradient != GRADIENT_DEFAULT_VALUE;
}

bool DEMElement::isElementsAccumulationEvaluated() {
	return elementsAccumulated != DISCRETE_ACC_VALUE || isSpring;
}

/* private functions evaluates directions, gradient, accumulation ... */

Direction::DirEnum DEMElement::findLocalSteepestDirection() {
	DEMElement* D8 = this;
	double grad = 0.0;
	double grad_t = 0.0;
	Direction::DirEnum dir = Direction::NONE;
	DEMElement* curr;
	for (unsigned int dir_t = 0; dir_t < MOLEM::Direction::NUMBER_OF_DIRECTION;
			dir_t++) {
		curr = &(static_cast<DEMElement&>(getNeighbour(
				Direction::allDirections[dir_t])));
		grad_t = evaluateGradient(*curr);
		if (grad_t > grad) {
			grad = grad_t;
			D8 = curr;
			dir = Direction::allDirections[dir_t];
		}
	}
	if (D8 == this) {
		markAsSink();
		gradient = GRADIENT_DEFAULT_VALUE;
	} else {
		setGoesInto(*D8);
		gradient = grad;
	}
	return dir;
}

void DEMElement::setGoesInto(DEMElement& g) {
	goesInto = &g;
}

void DEMElement::addDrainsFrom(DEMElement& g) {
	comesFrom.push_back(&g);
}

void DEMElement::markAsSpring() {
	if (comesFrom.size() > 1 || comesFrom.front() != 0x0) {
		comesFrom.clear();
	}
	isSpring = true;
}

void DEMElement::markAsSink() {
	if (goesInto != 0x0) {
		goesInto = 0x0;
		gradient = GRADIENT_DEFAULT_VALUE;
	}
	isSink = true;
}

double DEMElement::evaluateGradient(const DEMElement& g) const {
	return (getZvalue() - g.getZvalue()) / getCartesianDistance(g);
}

void DEMElement::setAccumulation(const DEMElement& source) {
	elementsAccumulated += source.elementsAccumulated;
}

} /* namespace MOLEM */

